Problem
昨天我們提到ListAdapter + DiffUtil
在一般RecyclerView
的基本使用。
而實際上工作中我們經常會需要在RecyclerView
上顯示不同的itemView
,
我們會添加Footer
、Header
,或是不同樣式的itemView
,
這時候該怎麼辦呢?
Solution
在submitList
之前,把dataList
中的item
順序重新編排後,
再使用submitList
提交我們的dataList
做更新。
實作
假設我們要做一個像Line一樣的聊天室,
而且底部要添加一個名為“已滑至底”的TextView
做為Footer
。
資料的部分會用Message
這個data class
來顯示訊息,
並根據isFromMe
來判斷是否為自己發送的訊息,展示不同的`itemView
data class Message(
val id: Long,
val timeStamp: Long,
val isFromMe: Boolean,
val message: String
)
首先,因為會展示不同的項目,我們不餵ListAdapter
吃data class
了,
改吃自己創建的sealed class
- 這邊我們命名為DataItem
。
class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {}
我們添加一個sealed class
,
這個sealed class
負責用來控管不同item的型態類別。
這邊我們列舉出Item
和Footer
這兩個型態(data type)。
因為DiffUtil
需要一個參數作為判斷新舊item是否一樣的依據。
所以我們創建一個abstract item id
作為interface回傳判別用的數據。
isFromMe
則是用來判斷顯示訊息在左側還是右側的item view
。
sealed class DataItem {
abstract val id: Long
abstract val isFromMe: Boolean
data class Item(val message: Message) : DataItem() {
override val id = message.id
override val isFromMe = message.isFromMe
}
object Footer : DataItem() {
override val id = Long.MIN_VALUE
override val isFromMe = false
}
}
(關於sealed class
後續文章有機會會講解,或是你也可以看這篇寫得很詳盡)
這邊因為Footer
只是作為靜態顯示layout
因此只使用object
而非data class
。
DiffUtil也改為判斷DataItem
中的id
class DiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
創建一個enum class列舉需展示的所有viewType
,
enum class ItemViewType {
MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
}
這邊列舉viewType
,是給Adapter判斷要展示哪個ViewHolder
來使用的。
因為我們有不同的型別需判斷,
所以我們要覆寫getItemViewType
。
鍵盤按下control
+o
,選擇getItemViewType
並覆寫他。
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.FromMe -> ItemViewType.MESSAGE_FROM_ME.ordinal
is DataItem.ToMe -> ItemViewType.MESSAGE_TO_ME.ordinal
is DataItem.Footer -> ItemViewType.FOOTER.ordinal
}
}
這邊返回的int是onCreateViewHolder
會用到的viewType
,繼續往下看下去。
onCreateViewHolder
與onBindViewHolder
判斷 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ItemViewType.MESSAGE_FROM_ME.ordinal -> FromMeViewHolder.from(parent)
ItemViewType.MESSAGE_TO_ME.ordinal -> ToMeViewHolder.from(parent)
else -> FooterViewHolder.from(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is FromMeViewHolder -> {
val data = getItem(position) as DataItem.Item
holder.bind(data.message)
}
is ToMeViewHolder -> {
val data = getItem(position) as DataItem.Item
holder.bind(data.message)
}
is FooterViewHolder -> {
}
}
}
ViewHolder
為FromMe
、ToMe
及Footer
創建對應的ViewHolder
class FromMeViewHolder private constructor(val binding: ItemAccountHistoryNextContentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(message: Message) {
itemView.apply {
tv_message_from_me.text = message.content
}
}
companion object {
fun from(parent: ViewGroup): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_from_me, parent, false)
return ToMeViewHolder(view)
}
}
}
class ToMeViewHolder (view: View) : RecyclerView.ViewHolder(view) {
fun bind(message: Message) {
itemView.apply {
tv_message_to_me.text = message.content
}
}
companion object {
fun from(parent: ViewGroup): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_to_me, parent, false)
return ToMeViewHolder(view)
}
}
}
class FooterViewHolder (view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_footer, parent, false)
return FooterViewHolder(view)
}
}
}
最後一步了!
現在因為ListAdapter
吃的是DataItem,
我們只要創建一個function - addFooterAndSubmitList
來取代原本的submitList
,
利用DataItem
整理過後再交給submitList
處理就大功告成。
private val adapterScope = CoroutineScope(Dispatchers.Default)
fun addFooterAndSubmitList(list: List<Message>) {
adapterScope.launch {
val items = list.map {
if (it.isFromMe) DataItem.FromMe(it)
else DataItem.ToMe(it)
} + listOf(DataItem.Footer)
withContext(Dispatchers.Main) { //update in main ui thread
submitList(items)
}
}
}
只要透過呼叫addFooterAndSubmitList
就能讓ListAdapter
成功運作,
讓DiffUtil
自動去篩選判斷更新的內容。
rvAdapter.addFooterAndSubmitList(dataList)
data class Message(
val id: Long,
val timeStamp: Long,
val isFromMe: Boolean,
val content: String
)
class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {
enum class ItemViewType {
MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
}
private val adapterScope = CoroutineScope(Dispatchers.Default)
fun addFooterAndSubmitList(list: List<Message>) {
adapterScope.launch {
val items = list.map {
if (it.isFromMe) DataItem.FromMe(it)
else DataItem.ToMe(it)
} + listOf(DataItem.Footer)
withContext(Dispatchers.Main) { //update in main ui thread
submitList(items)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ItemViewType.MESSAGE_FROM_ME.ordinal -> FromMeViewHolder.from(parent)
ItemViewType.MESSAGE_TO_ME.ordinal -> ToMeViewHolder.from(parent)
else -> FooterViewHolder.from(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is FromMeViewHolder -> {
val data = getItem(position) as DataItem.FromMe
holder.bind(data.message)
}
is ToMeViewHolder -> {
val data = getItem(position) as DataItem.ToMe
holder.bind(data.message)
}
is FooterViewHolder -> {
}
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.FromMe -> ItemViewType.MESSAGE_FROM_ME.ordinal
is DataItem.ToMe -> ItemViewType.MESSAGE_TO_ME.ordinal
is DataItem.Footer -> ItemViewType.FOOTER.ordinal
}
}
class FromMeViewHolder private constructor(val binding: ItemAccountHistoryNextContentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(message: Message) {
itemView.apply {
tv_message_from_me.text = message.content
}
}
companion object {
fun from(parent: ViewGroup): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_from_me, parent, false)
return ToMeViewHolder(view)
}
}
}
class ToMeViewHolder (view: View) : RecyclerView.ViewHolder(view) {
fun bind(message: Message) {
itemView.apply {
tv_message_to_me.text = message.content
}
}
companion object {
fun from(parent: ViewGroup): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_to_me, parent, false)
return ToMeViewHolder(view)
}
}
}
class FooterViewHolder (view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_footer, parent, false)
return FooterViewHolder(view)
}
}
}
class DiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
}
sealed class DataItem {
abstract val id: Long
abstract val isFromMe: Boolean
data class FromMe(val message: Message) : DataItem() {
override val id = message.id
override val isFromMe = message.isFromMe
}
data class ToMe(val message: Message) : DataItem() {
override val id = message.id
override val isFromMe = message.isFromMe
}
object Footer : DataItem() {
override val id = Long.MIN_VALUE
override val isFromMe = false
}
}